#include "ScriptManager.h"
#include "Log.h"
#include "World/World.h"
#include "World/Screen.h"
#include "Events.h"
#include "KeyboardHook.h"

using namespace System;
using namespace System::IO;
using namespace System::Reflection;
using namespace System::Globalization;
        
namespace GTA
{
	ScriptManager::ScriptManager()
	{


	}

	Object^ ScriptManager::InitializeLifetimeService()
	{
		// the descrition of InitializeLifetimeService() - "Give the AppDomain an infinate lifetime by preventing a lease from being created."
		// But .Net the fucker was still Garbage Collecting my marshal by ref object for some reason.
		// It was only doing it when left at esc menu for long time, i presume script thread is paused while at escape menu , so it was timing out the connection.
		// Returning null here seems to do the trick. Alternative would prolly be to create a new thread just to keep proxy alive for when other thread is paused at escape menu... fuck that.
		return nullptr; 
	}

	void ScriptManager::Init(String^ coreasmloc)
	{
		try
		{
			coreAsmLocation = coreasmloc;
			GTA::Log::Debug("SL Coreasmloc: "+coreAsmLocation);
			AppDomain::CurrentDomain->AssemblyResolve += gcnew ResolveEventHandler(currentDomain_AssemblyResolve);
			AppDomain::CurrentDomain->UnhandledException += gcnew UnhandledExceptionEventHandler(currentDomain_UnhandledException);
			AppDomain::CurrentDomain->DomainUnload += gcnew System::EventHandler( currentDomain_DomainUnload);

			GTA::Log::Log("GTAScript",false);
			GTA::Log::Debug("ScriptManager Init, coreasm: "+coreasmloc);
			ScriptManager::GameDirectory = System::Environment::CurrentDirectory;
			ScriptManager::ScriptFileDirectory = GameDirectory + "\\Scripts";
			ScriptManager::ScriptFilePattern = "*.Net.dll";
			runningScripts = gcnew System::Collections::Generic::List<ScriptBase^>();
			System::AppDomain^ currentDomain = AppDomain::CurrentDomain;
			_Instance = this;
		} 
		catch (Exception^ ex)
		{
			GTA::Log::Exception(ex);
		}
	}

	int ScriptManager::FindScriptAssemblies()
	{ // searches scripts directory for files to load
		//try
		int count =0;
		try
		{
			GTA::Log::Message("Searching for script assemblies!");
			if ( !Directory::Exists(ScriptFileDirectory) )
			{
				GTA::Log::Debug("Create scripts folder!");
				Directory::CreateDirectory(ScriptFileDirectory);
				return 0;
			}
			array<String^>^ files = Directory::GetFiles(ScriptFileDirectory, ScriptFilePattern, SearchOption::AllDirectories);
			GTA::Log::Debug("Found "+files->Length + " files in scripts directory.");
			for (int i=0;i<files->Length;i++)
			{
				GTA::Log::Debug("Searching scripts in assembly: "+files[i]);
				count += FindScriptsInAssembly(files[i]); // search for the script classes in assembly, then load them
				GTA::Log::Debug("Loaded "+count+" scripts from assembly: "+files[i]);

			}
		}
		catch (Exception^ ex)
		{
			GTA::Log::Exception(ex);
		}
		GTA::Log::Message("Load scrips complete! Loaded "+count+" scripts in total.");
		return count;
	};

	int ScriptManager::FindScriptsInAssembly( String^ assemblypath )
	{ // Find all of Script classes in DLL file
		int count = 0;
		try
		{
			Assembly^ as = nullptr;
			try
			{
				GTA::Log::Debug("Loading Script Assembly: "+assemblypath);
				as = Assembly::Load(File::ReadAllBytes(assemblypath));
			}
			catch (Exception^ e)
			{
				GTA::Log::Exception(e);
				return 0;
			}
			GTA::Log::Debug("Searching types in assembly: "+assemblypath);
			array<Type^>^ types;
			try
			{
				types = as->GetTypes(); // get all class types in dll
			}
			catch (ReflectionTypeLoadException^ ex)
			{ // prolly not a script of ours then
				GTA::Log::Exception(ex);
				for (int i=0;i<ex->LoaderExceptions->Length;i++)
				{
					array<System::Exception^,1>^ e = ex->LoaderExceptions;
					GTA::Log::Debug("--LoaderExceptions: " + e[i]->Message);
				}
				//types = ex->Types; // debug
				return 0;
			}
			catch (Exception^ ex)
			{
				GTA::Log::Exception(ex);
			}
			GTA::Log::Debug("Found "+types->Length+" types in assembly: "+assemblypath );
			for (int i=0;i<types->Length;i++)
			{
				GTA::Log::Debug("Type found: " + types[i]->Name);
			}
			for (int i=0;i<types->Length;i++)
			{
				try
				{
					//if (types[i]->IsAssignableFrom(GTA::ScriptBase::typeid)) 
					if (types[i]->IsSubclassOf(GTA::ScriptBase::typeid))//typeid
					{ // if is typeof Script
						GTA::Log::Debug("Found Script: "+types[i]->Name+" in assembly "+assemblypath);
						RegisterScript(types[i]); // register the script thread
						count++;
					}
					else
					{
						GTA::Log::Debug("Not a script! script: "+types[i]->FullName+", namespace: "+types[i]->Namespace);
					}
				}
				catch (Exception^ ex)
				{ 
					GTA::Log::Exception(ex);
				}
			}
		}
		catch (Exception^ ex)
		{
			GTA::Log::Exception(ex);
		}
		return count;
	}

	 ScriptBase^ ScriptManager::RegisterScript(Type^ scriptclass)
	 { // register the script, create instance
		try
		{
			GTA::Log::Message("Loading Script: "+scriptclass->FullName);
			Object^ obj = Activator::CreateInstance(scriptclass);
			ScriptBase^ scr = safe_cast<ScriptBase^>(obj);
			runningScripts->Add(scr);
			GTA::Log::Debug("Script "+scr->GetType()->FullName + " started.");
			return scr;
		}
		catch (Exception^ e)
		{
			GTA::Log::Exception(e);
			return nullptr;
		}
	};

	void ScriptManager::ReloadAllScripts()
	{ // better to just reload script domain instead
		UnloadAllScripts();
		FindScriptAssemblies();
		
	}

	void ScriptManager::UnloadAllScripts()
	{
		Log::Debug("ScriptManager.UnloadAllScripts");
        for each ( ScriptBase^ script in runningScripts )
        {
            UnloadScript(script);

        }
		GTA::Events::CleanUp::Invoke(nullptr,gcnew GTA::Events::CleanUpEventArgs(nullptr));
		GTA::Events::CleanUp::Call();
	}

	void ScriptManager::UnloadScript(ScriptBase^ script)
	{
		GTA::Events::CleanUp::Invoke(nullptr, gcnew GTA::Events::CleanUpEventArgs(script));
		GTA::Events::CleanUp::Call();
		script = nullptr;
	}
	

	void ScriptManager::InitalizeHooks(System::IntPtr DllHandle,System::UInt32 DllThreadId)
	{
		GTA::Log::Message("Attaching Keyboard Hook.");
		bool success = KeyboardHook::AttachHook(false,DllHandle,DllThreadId);
		if (!success) { success = KeyboardHook::AttachHook(false,DllHandle,0); }
		if (!success) GTA::Log::Warning("Attaching App Keyboard Hook Failed.");
		if (!success) { success = KeyboardHook::AttachHook(true,DllHandle,0); } // this global hook never seems to work when gta window has focus..
		if (!success) GTA::Log::Error("Attaching Global Keyboard Hook Failed.");
	}
	
	void ScriptManager::UnloadHooks()
	{
		KeyboardHook::DetachHook();
	}

	void ScriptManager::OnStart()
	{
		World::World();
		Screen::Screen();
		GTA::Events::OnStart::Invoke(nullptr,gcnew EventArgs());
	}

	void ScriptManager::CallEvents()
	{
		GTA::Events::OnStart::Call();
		GTA::Events::KeyDown::Call();
		GTA::Events::KeyUp::Call();
		GTA::Events::RunTick::Invoke(nullptr,gcnew EventArgs());
		GTA::Events::RunTick::Call();
		GTA::Events::CleanUp::Call();
	}

	Assembly^ ScriptManager::currentDomain_AssemblyResolve( Object^ sender, ResolveEventArgs^ args )
    {
		try
		{
			Log::Debug("ScriptManager AssemblyResolver");
			if(args->Name->Contains("GTAScript"))
			{
				Log::Debug("ScriptManager domain resolver returning core appdomain: "+args->Name);
				return Assembly::LoadFrom(coreAsmLocation);
			}
			else
			{
				Log::Error("ScriptManager Unable to retrieve AppDomain for: "+args->Name);
				return nullptr;
			}
		}
		catch (Exception^ ex)
		{
			Log::Exception(ex);
		}
		return nullptr;
    }

	void ScriptManager::currentDomain_UnhandledException( Object^ sender, UnhandledExceptionEventArgs^ e )
    {
		try
		{
			Log::Debug("ScriptManager.currentDomain_UnhandledException");
			GTA::Log::Exception(safe_cast<Exception^>(e->ExceptionObject));
		}
		catch (Exception^ ex)
		{
			Log::Exception(ex);
		}
    }


	void ScriptManager::currentDomain_DomainUnload( Object^ sender, EventArgs^ e )
    {
		Log::Debug("ScriptManager.currentDomain_DomainUnload.");
		if (_Instance == nullptr) return;
		GTA::Events::DomainUnload::Invoke(nullptr, gcnew EventArgs());
		GTA::Events::DomainUnload::Call(); // call this strait away from our current system thread
    }

};